﻿using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.IO;
using ArgusLib.Zip;
using ArgusLib.Xml;
using System.Drawing;
using System.Runtime.InteropServices;

namespace ArgusLib.BugReport
{
	public static class ExceptionHandler
	{
		#region Properties
		public static string ExceptionXmlRoot { get { return "Exception"; } }
		public static string ExceptionEntryName { get { return ExceptionHandler.ExceptionXmlRoot + ".xml"; } }
		public static string ScreenshotsEntryDirectory { get { return "Screenshots/"; } }
		public static string AddCrashInfoExceptionXmlRoot { get { return "AddCrashInfoException"; } }
		public static string AddCrashInfoExceptionEntryName { get { return ExceptionHandler.AddCrashInfoExceptionXmlRoot + ".xml"; } }
		public static string MinidumpEntryName { get { return "Minidump.dmp"; } }

		public static bool IsInitialized { get; private set; }
		public static FileInfo LockFile { get; private set; }
		public static FileInfo CrashInfoFile { get; private set; }
		public static MinidumpType MinidumpType { get; set; }
		#endregion

		#region Events
		public static event AddCrashInfoHandler AddCrashInfo;
		public static event CrashDetectedHandler CrashDetected;
		public static event CrashHandler Crash;
		#endregion

		#region Initializers
		static ExceptionHandler()
		{
			ExceptionHandler.IsInitialized = false;
			ExceptionHandler.MinidumpType = BugReport.MinidumpType.MiniDumpNormal;
		}

		public static void Initialize()
		{
			ExceptionHandler.Initialize(null, null);
		}

		public static void Initialize(FileInfo lockFile, FileInfo crashInfoFile)
		{
			if (ExceptionHandler.IsInitialized == true)
				throw new InvalidOperationException("ExceptionHandler has already been initialized.");

			ExceptionHandler.IsInitialized = true;

			if (lockFile == null)
				lockFile = new FileInfo("lock");
			if (crashInfoFile == null)
				crashInfoFile = new FileInfo("CrashInfo.zip");

			ExceptionHandler.LockFile = lockFile;
			ExceptionHandler.CrashInfoFile = crashInfoFile;

			string[] crashInfo = null;
			if (ExceptionHandler.LockFile.Exists == true)
				crashInfo = File.ReadAllLines(ExceptionHandler.LockFile.FullName);
			ExceptionHandler.LockFile.TryCreate();

			AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
			Application.ThreadException += Application_ThreadException;
			AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;

			if (crashInfo != null && crashInfo.Length > 1 && ExceptionHandler.CrashDetected != null)
			{
				ExceptionHandler.OnCrashDetected(crashInfo[0], crashInfo[1]);
			}
		}
		#endregion

		#region Private Methods
		static void OnCrashDetected(string crashInfoPath, string baseDir)
		{
			FileInfo crashInfoFile = new FileInfo(crashInfoPath);
			if (crashInfoFile.Exists == false)
				return;
			ExceptionInfo exception = null;
			List<Image> screenshots = new List<Image>();
			using (TempDirectory tempDir = TempDirectory.Create())
			{
				if (crashInfoFile.Exists == true)
				{
					using (ZipFile zipFile = ZipFile.Read(crashInfoFile.FullName))
					{
						ZipEntry entry = zipFile[baseDir+ExceptionHandler.ExceptionEntryName];
						if (entry != null)
						{
							using (Stream stream = entry.OpenReader())
							{
								using (Deserializer deserializer = Deserializer.FromStream(stream))
								{
									exception = deserializer.Deserialize() as ExceptionInfo;
								}
							}
						}
						zipFile.FlattenFoldersOnExtract = true;
						zipFile.ExtractSelectedEntries("*", baseDir+ExceptionHandler.ScreenshotsEntryDirectory, tempDir.Path);
						FileInfo[] files = tempDir.DirectoryInfo.GetFiles();
						foreach (FileInfo file in files)
						{
							Image image = Image.FromFile(file.FullName);
							screenshots.Add(image);
						}
					}
				}
				CrashDetectedArgs e = new CrashDetectedArgs(crashInfoFile, exception, screenshots.ToArray());
				ExceptionHandler.CrashDetected(e);
				foreach (Image image in e.Screenshots)
					image.Dispose();
				tempDir.Dispose();

				if (e.Protocol != null)
				{
					e.Protocol.SendAsync(crashInfoFile, new Action<bool>((sent) =>
						{
							if (sent == true)
								crashInfoFile.Delete();
						}));
				}
			}
		}

		static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
		{
			ExceptionHandler.WriteCrashInfoAndTerminate(e.Exception);
		}

		static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			ExceptionHandler.WriteCrashInfoAndTerminate(e.ExceptionObject);
		}

		public static void WriteCrashInfoAndTerminate(object exceptionObject)
		{
			AppDomain.CurrentDomain.ProcessExit -= ExceptionHandler.CurrentDomain_ProcessExit;
			string baseDir = DateTime.Now.ToBinary().ToString() + '/';

			try { File.WriteAllLines(ExceptionHandler.LockFile.FullName, new string[] { ExceptionHandler.CrashInfoFile.FullName, baseDir }); }
			catch { }

			char[] invalidChars = Path.GetInvalidFileNameChars();

			using (TempDirectory tempDir = TempDirectory.Create())
			{
				using (ZipFile zipFile = new ZipFile(ExceptionHandler.CrashInfoFile.FullName))
				{
					zipFile.CompressionMethod = CompressionMethod.Deflate;
					zipFile.CompressionLevel = Zlib.CompressionLevel.BestCompression;

					if (MiniDumpWriter.IsAvailable == true)
					{
						FileInfo tempFile = tempDir.GetTempFile();
						if (MiniDumpWriter.WriteMiniDumpExtern(tempFile.FullName, ExceptionHandler.MinidumpType) == true)
						{
							ZipEntry entry = zipFile.AddFile(tempFile.FullName);
							entry.FileName = baseDir + ExceptionHandler.MinidumpEntryName;
						}
					}

					if (exceptionObject is ArgusLib.Xml.ISerializable)
					{
						zipFile.AddEntry(baseDir + ExceptionHandler.ExceptionEntryName, new WriteDelegate((name, stream) =>
						{
							using (Serializer serializer = Serializer.FromStream(stream, ExceptionHandler.ExceptionXmlRoot))
							{
								serializer.Serialize(exceptionObject);
								stream.Flush();
							}
						}));
					}
					if (exceptionObject is Exception)
					{
						zipFile.AddEntry(baseDir + ExceptionHandler.ExceptionEntryName, new WriteDelegate((name, stream) =>
							{
								using (Serializer serializer = Serializer.FromStream(stream, ExceptionHandler.ExceptionXmlRoot))
								{
									serializer.Serialize(new ExceptionInfo((Exception)exceptionObject));
									stream.Flush();
								}
							}));
					}
					else if (exceptionObject != null)
					{
						zipFile.AddEntry(baseDir + ExceptionHandler.ExceptionEntryName, exceptionObject.ToString());
					}
					foreach (Form form in Application.OpenForms)
					{
						using (Bitmap screenshot = new Bitmap(form.Width, form.Height))
						{
							if (form.InvokeRequired == false)
								form.DrawToBitmap(screenshot, new Rectangle(0, 0, form.Width, form.Height));
							else
								form.Invoke(new Action(() => { form.DrawToBitmap(screenshot, new Rectangle(0, 0, form.Width, form.Height)); }));
							FileInfo tempFile = tempDir.GetTempFile(". " + form.Text.RemoveChars(invalidChars) + ".png");
							screenshot.Save(tempFile.FullName, System.Drawing.Imaging.ImageFormat.Png);
							zipFile.AddFile(tempFile.FullName, baseDir + ExceptionHandler.ScreenshotsEntryDirectory);
						}
					}

					zipFile.Save();
					if (ExceptionHandler.AddCrashInfo != null)
					{
						try { ExceptionHandler.AddCrashInfo(new AddCrashInfoArgs(zipFile)); }
						catch (Exception addInfoException)
						{
							zipFile.AddEntry(baseDir + ExceptionHandler.AddCrashInfoExceptionEntryName, new WriteDelegate((name, stream) =>
								{
									using (Serializer serializer = Serializer.FromStream(stream, ExceptionHandler.AddCrashInfoExceptionXmlRoot))
									{
										serializer.Serialize(new ExceptionInfo(addInfoException));
										stream.Flush();
									}
								}));
						}
						zipFile.Save();
					}
				}
				tempDir.Dispose();
			}
			if (ExceptionHandler.Crash != null)
				ExceptionHandler.Crash(new CrashArgs(exceptionObject as Exception));
			System.Environment.Exit(0);
		}

		static void CurrentDomain_ProcessExit(object sender, EventArgs e)
		{
			if (ExceptionHandler.LockFile != null)
				ExceptionHandler.LockFile.TryDelete();
		}
		#endregion
	}
}
